package defectsim;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.vecmath.Vector2d;

import repast.simphony.context.Context;
import repast.simphony.engine.environment.RunEnvironment;
import repast.simphony.parameter.Parameters;
import repast.simphony.space.continuous.ContinuousSpace;
import repast.simphony.space.continuous.NdPoint;
import repast.simphony.space.grid.Grid;
import repast.simphony.valueLayer.GridValueLayer;
import defectsim.agents.DefectAgent;
import defectsim.agents.Dislocation;
import defectsim.agents.PointDefect;
import defectsim.material.Material;
import defectsim.material.MaterialFactory;
import defectsim.material.MaterialFactory.MaterialName;
import defectsim.resources.AllForces;
import defectsim.resources.DefectResource;
import defectsim.resources.Force;

public class SimulationEnvironment {

	private Context<BaseAgent> context;

	// Simulation parameters
	private double boxLength;
	private int gridSize;
	private double gridScale;
	private Material material;
	private double timeStep;
	private double initialTimeStep;
	private double minCutoff;


	private double strain = 0;
	private double strainRate;
	private double meanVelocity;
	private double burgersVector;

	private boolean saveData;

	private boolean neighbourListUpdate = true;
	private int neighbourListUpdateCount;

	private boolean nucleation;

	private SimulationMetrics metrics;

	/*
	 *  Simulated environment class is singleton
	 */
	private SimulationEnvironment(){

	}

	private static SimulationEnvironment _instance = null;

	static public SimulationEnvironment getInstance() {
		if(_instance == null) {
			_instance = new SimulationEnvironment();
		}
		return _instance;
	}

	public void initialize(){
		Parameters parameters = this.getParameters();
		int material = (Integer)parameters.getValue("material");
		int activeSlip = (Integer)parameters.getValue("activeslipsystems");
		switch(material){
		case 0: 
			this.material = MaterialFactory.createMaterial(MaterialName.Copper, activeSlip);
			break;
		case 1: 
			this.material = MaterialFactory.createMaterial(MaterialName.Aluminum, activeSlip);
			break;
		}
		this.gridSize = (Integer)parameters.getValue("gridsize");
		this.timeStep = (Double)parameters.getValue("timestep") * Globals.PICOSECONDS;
		this.initialTimeStep = timeStep;
		this.boxLength = (Double)parameters.getValue("spacelength");
		this.gridScale = gridSize / boxLength;
		this.burgersVector = this.material.getBurgersVector();
		this.minCutoff = (Double)parameters.getValue("mincutoffradius") * burgersVector * Globals.NANOMETER_TO_MICROMETER;
		this.saveData = (Boolean)parameters.getValue("savedata");
		this.metrics = new SimulationMetrics();
		this.nucleation = false;
	}

	public Parameters getParameters() {
		return RunEnvironment.getInstance().getParameters();
	}

	public Material getMaterial() {
		return material;
	}

	public double getTimeStep() {
		return timeStep;
	}

	public void setContext(Context<BaseAgent> context) {
		this.context = context;
	}

	public Context<BaseAgent> getContext(){
		return context;
	}

	public double getCurrentTick(){
		return RunEnvironment.getInstance().getCurrentSchedule().getTickCount();
	}

	public NdPoint getLocation(DefectAgent da){
		return new NdPoint(da.getX(), da.getY());
	}

	public GridValueLayer getValueLayer(String valueLayerName){
		return (GridValueLayer) context.getValueLayer(valueLayerName);
	}

	@SuppressWarnings("unchecked")
	public AllForces getForces() {
		AllForces forces = new AllForces();
		Context drc = context.getSubContext(Globals.DEFECT_RESOURCE_CONTEXT_NAME);
		List<DefectResource> resources = ((DefectResourceContext)drc).getResources();
		for (Object r : resources) 
			if (r instanceof Force){
				Force f = (Force)r;
				forces.addForce(f);
			}

		return forces;
	}

	@SuppressWarnings("unchecked")
	public void getNeighbours(Dislocation agent, List<Dislocation> longRangeNeighbours, List<Dislocation> midRangeNeighbours, 
			List<Dislocation> shortRangeNeighbours, List<PointDefect> pointDefects, double longPerceptionRadius, 
			double midPerceptionRadius, double shortPerceptionRadius){
		ContinuousSpace space = (ContinuousSpace) context.getProjection(Globals.BOX_NAME);
		NdPoint p1 = space.getLocation(agent);
		Iterable<DefectAgent> allNeighbours = space.getObjects();
		for (Object defect : allNeighbours) {
			if (defect instanceof Dislocation){
				Dislocation d = (Dislocation) defect;
				NdPoint p2 = space.getLocation(d);
				double dist = Util.calculateMICDistance(p1, p2, boxLength);
				if ((dist > 0.0) && (dist <= shortPerceptionRadius))
					shortRangeNeighbours.add(d);
				if ((dist > 0.0) && (dist <= midPerceptionRadius))
					midRangeNeighbours.add(d);
				if ((dist > 0.0) && (dist <= longPerceptionRadius))
					longRangeNeighbours.add(d);
			}
			if (defect instanceof PointDefect){
				PointDefect pointDefect = (PointDefect) defect;
				NdPoint p2 = space.getLocation(pointDefect);
				double dist = Util.calculateMICDistance(p1, p2, boxLength);
				if ((dist > 0.0) && (dist <= midPerceptionRadius))
					pointDefects.add(pointDefect);

			}
		}
	}

	@SuppressWarnings("unchecked")
	public List<Dislocation> getDislocations(){
		List<Dislocation> neighbours = new ArrayList<Dislocation>();
		ContinuousSpace space = (ContinuousSpace) context.getProjection(Globals.BOX_NAME);
		Iterable<Dislocation> allNeighbours = space.getObjects();
		for (Object defect : allNeighbours) 
			if (defect instanceof Dislocation)				
				neighbours.add((Dislocation)defect);
		return neighbours;
	}
	
	@SuppressWarnings("unchecked")
	public List<PointDefect> getPointDefects(){
		List<PointDefect> neighbours = new ArrayList<PointDefect>();
		ContinuousSpace space = (ContinuousSpace) context.getProjection(Globals.BOX_NAME);
		Iterable<PointDefect> allNeighbours = space.getObjects();
		for (Object defect : allNeighbours) 
			if (defect instanceof PointDefect)				
				neighbours.add((PointDefect)defect);
		return neighbours;
	}

	@SuppressWarnings("unchecked")
	public void moveByVector(DefectAgent da, double distance, double direction){
		ContinuousSpace space = (ContinuousSpace) context.getProjection(Globals.BOX_NAME);
		Grid grid = (Grid)context.getProjection(Globals.GRID_NAME);
		double dx = distance * Math.cos(direction);
		double dy = distance * Math.sin(direction);
		space.moveByDisplacement(da, dx, dy, 0);
		NdPoint point = space.getLocation(da);
		grid.moveTo(da, (int)(point.getX() * gridScale), (int)(point.getY() * gridScale));
		da.setX(point.getX());
		da.setY(point.getY());
	}

	@SuppressWarnings("unchecked")
	public void moveTo(DefectAgent da, NdPoint p){
		ContinuousSpace space = (ContinuousSpace) context.getProjection(Globals.BOX_NAME);
		Grid grid = (Grid)context.getProjection(Globals.GRID_NAME);
		double dx = p.getX();
		double dy = p.getY();
		space.moveTo(da, dx, dy, 0);
		grid.moveTo(da, (int)(dx * gridScale), (int)(dy * gridScale));
	}


	public void annihilate(DefectAgent da){
		if (context.contains(da)) {
			context.remove(da);
			metrics.setAnnihilated(metrics.getAnnihilated() + 1);
		}
	}

	public void nucleate(DefectAgent da){
		context.add(da);
		metrics.setNucleated(metrics.getNucleated() + 1);
	}

	public void add(PointDefect pointDefect){
		context.add(pointDefect);
	}

	public boolean isNeighbourListUpdate(){
		return neighbourListUpdate;
	}

	public double getStrain(){
		return strain;
	}
	
	public double getMeanVelocity(){
		return meanVelocity;
	}

	public double getStrainRate() {
		return strainRate;
	}

	public boolean isNucleation() {
		return nucleation;
	}

	public void setNucleation(boolean nucleation) {
		this.nucleation = nucleation;
	}

	public SimulationMetrics getMetrics(){
		return metrics;
	}

	@SuppressWarnings("unchecked")
	public void update(){

		if (this.getCurrentTick() == 1.0) {
			if (saveData) {
				metrics.setFiles();
				try {
					metrics.savePositionDataPointSource(this.getPointDefects());
				} catch (IOException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
				try {
					metrics.saveInputData(this.getParameters());
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			this.neighbourListUpdate = true;
			neighbourListUpdateCount = 0;
		}
		if (this.getCurrentTick() > 1){
			GridValueLayer vl = (GridValueLayer)context.getValueLayer(Globals.STRESS_VALUE_LAYER);
			for (int i = 0; i < gridSize; i++) 
				for (int j = 0; j < gridSize; j++) {
					Vector2d totalStress = new Vector2d();
					int nDislocationsAtGridPoint = 0;
					Grid grid = (Grid) context.getProjection(Globals.GRID_NAME);
					Iterable<Dislocation> allDefects = grid.getObjectsAt(i, j);
					for (Object defect : allDefects) 
						if (defect instanceof Dislocation){
							Dislocation dislocation = (Dislocation) defect;
							nDislocationsAtGridPoint++;
							totalStress.add(dislocation.getEffectiveStress());
						}
					double meanStressAtGridPoint = 0.0;
					if (nDislocationsAtGridPoint > 0)
						meanStressAtGridPoint = totalStress.x / nDislocationsAtGridPoint;
					vl.set(meanStressAtGridPoint, i ,j);
				}

			ContinuousSpace space = (ContinuousSpace) context.getProjection(Globals.BOX_NAME);

			Iterable<Dislocation> allDefects = space.getObjects();
			if ((this.getCurrentTick() % 10) == 0.0)
				try {
					if (saveData)
						metrics.savePositionData(allDefects, (int)this.getCurrentTick());
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			metrics.update(allDefects, boxLength);
			strain = metrics.getStrain();
			strainRate = metrics.getStrainRate();
			meanVelocity = metrics.getMeanVelocity();
			if (metrics.getMinTimeStep() > initialTimeStep)
				timeStep = metrics.getMinTimeStep();
			metrics.printTickData();
			if (saveData)
				metrics.saveTickData();

			/*
			 *  Verlet neighbour list update
			 */
			neighbourListUpdateCount++;
			double deltaR = neighbourListUpdateCount * timeStep * metrics.getMaxVelocity() * Globals.METER_TO_MICROMETER;
			if (deltaR > minCutoff) {
				neighbourListUpdate = true;
				neighbourListUpdateCount = 0;
			}
			else 
				neighbourListUpdate = false;
		}
	}

	public void saveData() throws IOException{
		if (saveData){
			metrics.closeFiles();
		}
	}

}
